React Native for Web + TypeScriptを使ってStorybook公式のチュートリアルをやってみた
Storybookとは、React、Vue、Angular 用の UI コンポーネントを開発・管理するためのオープンソースツールです。 公式ページにあるチュートリアルを、普段の業務で使用しているReact Native for WebとTypeScriptを使ってやってみたのでご紹介します。
実装するもの
- Storybook 公式ページのチュートリアルの タスク管理アプリ(当記事では、タスク一覧の表示までをご紹介します。)
今回作成したソースコードの一式はこちらにあります。
実装方針
- Create React App を使ってプロジェクトを作成する
- TypeScript を使用する
- 画面の作成に、React Native for Webのコンポーネントを使用する
- チュートリアルの内容(*2020/6/12 時点)に沿う *忠実に再現したものではなく、適時変更を加えている点をご了承ください。
プロジェクトの作成
Create React App
を使って React アプリを作成し、TypeScriptとReact Native for Webを導入します。今回は以下の記事でご紹介した方法で React アプリを作成し、TypeScript
、React Native for Web
、Prettier
を導入しました。
* 以下、React Native for Web は RNfW と記載します。
Get Started
Storybook を導入する
以下のコマンドを実行し、Storybook を導入します。
$ npx -p @storybook/cli sb init
このコマンドによって.storybookフォルダ
、src/storiesフォルダ
が自動で作成されます。
処理が終了したら、yarn storybook
を実行し、Storybook が立ち上がるのを確認しましょう。
TypeScript、RNfW を実行するための設定を追加する
src/storiesフォルダ
内に作られた story ファイルの拡張子を見ると.js
となっています。こちらを.tsx
としても実行できるようにします。
.storybook/main.js
のstories: ['../src/**/*.stories.js']
の箇所をstories: ['../src/**/*.stories.tsx']
と変更しましょう。
module.exports = { stories: ['../src/**/*.stories.tsx'], .... } }
src/storiesフォルダ
内の.stories.js
ファイルを.stories.tsx
と拡張子を変更し、yarn storybook
で実行できることを確認します。
また、今回 RNfW を使用するため、.storybookフォルダ
内に、webpack.config.js
を作成し、以下の通り記載しました。
module.exports = { resolve: { alias: { 'react-native$': 'react-native-web' } }}
*設定内容についてこちらの記事を参照させていただきました。
ここまでで、チュートリアルを始める準備は完了です。
Build a simple component
まず、タスク1件の情報を表示する Task コンポーネントを作成します。
このコンポーネントは「チェックボックス」「タスクタイトル」「お気に入りチェック用の ☆ マーク」で構成されています。
それぞのタスクはDefault(TASK_INBOX)/ Pinned(TASK_PINNED)/ Archived(TASK_ARCHIVED)
の3つの状態を持ち、異なる UI を表示します。
src/components/Task.tsx
import React from 'react' import { StyleSheet, View, Text, TouchableOpacity } from 'react-native' const styles = StyleSheet.create({ container: { height: 50, borderWidth: 1, borderColor: 'black', justifyContent: 'center', }, rowContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, checkAndTitle: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', }, checkbox: { marginLeft: 24, fontSize: 24, fontWeight: 'bold', }, title: { marginLeft: 24, fontSize: 16, fontWeight: 'bold', }, star: { marginRight: 24, fontSize: 18, }, }) export type TaskProps = { task: { id: string title: string state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED' } onArchiveTask: (id: string) => void onPinTask: (id: string) => void } function Task(props: TaskProps) { const { task: { id, title, state }, onArchiveTask, onPinTask, } = props return ( <View style={styles.container}> <View style={styles.rowContainer}> <View style={styles.checkAndTitle}> <TouchableOpacity onPress={() => onArchiveTask(id)}> <Text style={styles.checkbox}> {state === 'TASK_ARCHIVED' ? '☑︎' : '□'} </Text> </TouchableOpacity> <Text style={styles.title}>{title}</Text> </View> {state !== 'TASK_ARCHIVED' && ( <TouchableOpacity onPress={() => onPinTask(id)}> <Text style={styles.star}> {state === 'TASK_PINNED' ? '★' : '☆'} </Text> </TouchableOpacity> )} </View> </View> ) } export default Task
続いて、Default(TASK_INBOX)/ Pinned(TASK_PINNED)/ Archived(TASK_ARCHIVED)
それぞれの状態の UI を Storybook で確認できるよう story ファイルを作成します。
src/components/Task.stories.tsx
import React from 'react' import { action } from '@storybook/addon-actions' import Task from './Task' export default { component: Task, title: 'Task', excludeStories: /.*Data$/, } export const taskData: { id: string title: string state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED' } = { id: '1', title: 'Test Task', state: 'TASK_INBOX', } export const actionsData = { onPinTask: action('onPinTask'), onArchiveTask: action('onArchiveTask'), } export const Default = () => <Task task={{ ...taskData }} {...actionsData} /> export const Pinned = () => ( <Task task={{ ...taskData, state: 'TASK_PINNED' }} {...actionsData} /> ) export const Archived = () => ( <Task task={{ ...taskData, state: 'TASK_ARCHIVED' }} {...actionsData} /> )
yarn storybook
を実行します。
「Default(チェック:なし、星:☆ を表示)」「Pinned(チェック:なし、星:★ を表示)」「Archived(チェック:あり、星:表示なし)」 と、それぞれの状態での UI が Storybook 上で確認できるようになりました。
Assemble a composite component
続いて、タスクを一覧表示するための TaskList コンポーネントを作成します。
src/components/TaskList.stories.tsx
import React from 'react' import { View, Text } from 'react-native' import Task from './Task' export type TaskListProps = { tasks: { id: string title: string state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED' }[] onArchiveTask: (id: string) => void onPinTask: (id: string) => void } function TaskList(props: TaskListProps) { const { tasks, onArchiveTask, onPinTask } = props if (tasks.length === 0) { return ( <View> <Text>You have no task.</Text> </View> ) } return ( <View> {tasks.map(task => ( <Task key={task.id} task={task} onPinTask={onPinTask} onArchiveTask={onArchiveTask} /> ))} </View> ) } export default TaskList
今回は「タスク6件全てが Default 状態の TaskList」「最後の1件のタスクが Pinned 状態の TaskList」「最後の1件のタスクが Archived 状態の TaskList」「タスクがない場合の TaskList」の それぞれの UI が確認できるように story ファイルを作成しました。
src/TaskList.stories.tsx
import React from 'react' import TaskList from './TaskList' import { taskData, actionsData } from './Task.stories' export default { component: TaskList, title: 'TaskList', decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>], excludeStories: /.*Data$/, } export const defaultTasksData = [ { ...taskData, id: '1', title: 'Task 1' }, { ...taskData, id: '2', title: 'Task 2' }, { ...taskData, id: '3', title: 'Task 3' }, { ...taskData, id: '4', title: 'Task 4' }, { ...taskData, id: '5', title: 'Task 5' }, { ...taskData, id: '6', title: 'Task 6' }, ] export const withPinnedTasksData: { id: string title: string state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED' }[] = [ ...defaultTasksData.slice(0, 5), { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, ] export const withArchivedTasksData: { id: string title: string state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED' }[] = [ ...defaultTasksData.slice(0, 5), { id: '6', title: 'Task 6 (archived)', state: 'TASK_ARCHIVED' }, ] export const Default = () => ( <TaskList tasks={defaultTasksData} {...actionsData} /> ) export const WithPinnedTasks = () => ( <TaskList tasks={withPinnedTasksData} {...actionsData} /> ) export const WithArchivedTasks = () => ( <TaskList tasks={withArchivedTasksData} {...actionsData} /> ) export const Empty = () => <TaskList tasks={[]} {...actionsData} />
再度 yarn storybook
で、Storybook を起動します。
TaskList コンポーネントのそれぞれの状態での UI が確認できるようになりました。
まとめ
Storybook 公式のチュートリアルを React Native for Web + TypeScript で実装した内容をご紹介しました。
今回自身の入門としてチュートリアルに取り組みましたが、コンポーネントの UI を一覧形式で管理でき、それぞれの状態による変化も非常に把握しやすくなる点など、Storybook を導入するメリットを感じることができました。Addon の機能などについても、今後さらに調べていければと思います。
この記事がどなたかのお役に立てば幸いです。